diff options
Diffstat (limited to 'app/[lng]/partners/(partners)/pq_new/page.tsx')
| -rw-r--r-- | app/[lng]/partners/(partners)/pq_new/page.tsx | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/pq_new/page.tsx b/app/[lng]/partners/(partners)/pq_new/page.tsx new file mode 100644 index 00000000..eea5b21d --- /dev/null +++ b/app/[lng]/partners/(partners)/pq_new/page.tsx @@ -0,0 +1,298 @@ +import * as React from "react"; +import Link from "next/link"; +import { Metadata } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { LogIn, Edit, Eye, Ellipsis } from "lucide-react"; +import { Shell } from "@/components/shell"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { unstable_noStore as noStore } from 'next/cache'; +import { getAllPQsByVendorId, getPQStatusCounts } from "@/lib/pq/service"; +import { InformationButton } from "@/components/information/information-button"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + +export const metadata: Metadata = { + title: "사전 평가 (PQ) 목록", + description: "요청된 사전 평가 목록을 확인하고 작성합니다.", +}; + +// 페이지가 기본적으로 동적임을 나타냄 +export const dynamic = "force-dynamic"; + +function getStatusBadge(status: string) { + switch (status) { + case "REQUESTED": + return <Badge variant="outline">요청됨</Badge>; + case "IN_PROGRESS": + return <Badge variant="secondary">진행 중</Badge>; + case "SUBMITTED": + return <Badge variant="default">제출됨</Badge>; + case "APPROVED": + return <Badge variant="default">승인됨</Badge>; + case "REJECTED": + return <Badge variant="destructive">거부됨</Badge>; + default: + return <Badge variant="outline">{status}</Badge>; + } +} + +function getFormattedDate(date: Date | null) { + if (!date) return "-"; + return new Intl.DateTimeFormat("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }).format(new Date(date)); +} + +export default async function PQListPage() { + // 캐시 비활성화 + noStore(); + + // 인증 확인 + const session = await getServerSession(authOptions); + + // 로그인 확인 + if (!session || !session.user) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 사전 평가 (PQ) 목록 + </h2> + <p className="text-muted-foreground"> + 요청된 사전 평가 목록을 확인하고 작성합니다. + </p> + </div> + </div> + + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 사전 평가를 확인하려면 먼저 로그인하세요. + </p> + <Button size="lg" asChild> + <Link href="/partners?callbackUrl=/partners/pq"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인하기 + </Link> + </Button> + </div> + </div> + </Shell> + ); + } + + // 세션에서 vendorId 가져오기 + const vendorId = session.user.companyId; + + // 벤더 권한 확인 + if (session.user.domain !== "partners" || !vendorId) { + return ( + <Shell className="gap-6"> + <div className="flex items-center justify-between"> + <div> + <h2 className="text-2xl font-bold tracking-tight"> + 접근 권한 없음 + </h2> + </div> + </div> + <div className="flex flex-col items-center justify-center py-12 text-center"> + <div className="rounded-lg border border-dashed p-10 shadow-sm"> + <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3> + <p className="mb-6 text-muted-foreground"> + 벤더 계정으로 로그인해주세요. + </p> + </div> + </div> + </Shell> + ); + } + + const idAsNumber = Number(vendorId); + + // 데이터 가져오기 (병렬 실행) + const [pqList, pqStatusCounts] = await Promise.all([ + getAllPQsByVendorId(idAsNumber), + getPQStatusCounts(idAsNumber), + ]); + + return ( + <Shell className="gap-6"> + <div className="flex justify-between items-center"> + <div> + <div className="flex items-center gap-2"> + <h2 className="text-2xl font-bold tracking-tight">사전 평가 (PQ) 목록</h2> + <InformationButton pagePath="partners/pq_new" /> + </div> + <p className="text-muted-foreground"> + 요청된 사전 평가 목록을 확인하고 작성합니다. + </p> + </div> + </div> + + {/* PQ 상태 요약 카드 */} + <div className="grid gap-4 md:grid-cols-4"> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">총 PQ</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {Object.values(pqStatusCounts).reduce((sum, count) => sum + count, 0)}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">작성 대기</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {(pqStatusCounts.REQUESTED || 0) + (pqStatusCounts.IN_PROGRESS || 0)}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">제출됨</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {pqStatusCounts.SUBMITTED || 0}건 + </div> + </CardContent> + </Card> + <Card> + <CardHeader className="py-4"> + <CardTitle className="text-base">승인됨</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold"> + {pqStatusCounts.APPROVED || 0}건 + </div> + </CardContent> + </Card> + </div> + + {/* PQ 목록 테이블 */} + <Card> + <CardHeader> + <CardTitle>PQ 목록</CardTitle> + </CardHeader> + <CardContent className="p-0"> + <Table> + <TableHeader> + <TableRow> + <TableHead>유형</TableHead> + <TableHead>PQ 번호</TableHead> + <TableHead>프로젝트</TableHead> + <TableHead>상태</TableHead> + <TableHead>요청일</TableHead> + <TableHead>제출일</TableHead> + <TableHead>승인일</TableHead> + <TableHead>액션</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {pqList.length === 0 ? ( + <TableRow> + <TableCell colSpan={8} className="text-center py-8 text-muted-foreground"> + 요청된 PQ가 없습니다. + </TableCell> + </TableRow> + ) : ( + pqList.map((pq) => { + const canEdit = ["REQUESTED", "IN_PROGRESS", "REJECTED"].includes(pq.status); + const canView = ["SUBMITTED", "APPROVED"].includes(pq.status); + + return ( + <TableRow key={pq.id}> + <TableCell> + <Badge variant={ + pq.type === "PROJECT" ? "default" : + pq.type === "NON_INSPECTION" ? "secondary" : + "outline" + }> + {pq.type === "PROJECT" ? "프로젝트" : + pq.type === "NON_INSPECTION" ? "미실사" : + "일반"} + </Badge> + </TableCell> + <TableCell> + {pq.pqNumber || "-"} + </TableCell> + <TableCell> + {pq.projectName || "-"} + </TableCell> + <TableCell> + {getStatusBadge(pq.status)} + </TableCell> + <TableCell> + {getFormattedDate(pq.createdAt)} + </TableCell> + <TableCell> + {getFormattedDate(pq.submittedAt)} + </TableCell> + <TableCell> + {getFormattedDate(pq.approvedAt)} + </TableCell> + <TableCell> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + aria-label="액션 메뉴 열기" + variant="ghost" + className="flex size-8 p-0 data-[state=open]:bg-muted" + > + <Ellipsis className="size-4" aria-hidden="true" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end" className="w-36"> + {canEdit && ( + <DropdownMenuItem asChild> + <Link href={`/partners/pq_new/${pq.id}`}> + <Edit className="mr-2 h-4 w-4" /> + 작성 + </Link> + </DropdownMenuItem> + )} + {canView && ( + <DropdownMenuItem asChild> + <Link href={`/partners/pq_new/${pq.id}`}> + <Eye className="mr-2 h-4 w-4" /> + 보기 + </Link> + </DropdownMenuItem> + )} + </DropdownMenuContent> + </DropdownMenu> + </TableCell> + </TableRow> + ); + }) + )} + </TableBody> + </Table> + </CardContent> + </Card> + </Shell> + ); +}
\ No newline at end of file |
